/*
 * sys_call.s 包含系统调用底层处理过程（主要是系统调用中断入口处理，查找中断调用入口，中断返回处理等）
 * 除了系统调用，这个文件也包含了定时中断处理过程（部分代码可以和系统中断共用），以及
 *      软盘中断处理过程和硬盘中断处理过程
 * 注意：这里也会进行信号处理，每次定时中断或者系统调用结束前，都会检查是否需要进行信号处理
 *      普通中断中不进行信号处理，因为这会将中断过程搞混乱。
 *
 * ret_from_sys_call中的栈布局
 *
 *	 0(%esp) - %eax
 *	 4(%esp) - %ebx
 *	 8(%esp) - %ecx
 *	 C(%esp) - %edx
 *	10(%esp) - original %eax	(如果不是系统调用，该处值为-1)
 *	14(%esp) - %fs
 *	18(%esp) - %es
 *	1C(%esp) - %ds
 *	20(%esp) - %eip
 *	24(%esp) - %cs
 *	28(%esp) - %eflags
 *	2C(%esp) - %oldesp
 *	30(%esp) - %oldss
 */

SIG_CHLD= 17
EAX		= 0x00
EBX		= 0x04
ECX		= 0x08
EDX		= 0x0C
ORIG_EAX= 0x10
FS		= 0x14
ES		= 0x18
DS		= 0x1C
EIP		= 0x20
CS		= 0x24	# 原cs段值在栈中的偏移
EFLAGS	= 0x28
OLDESP	= 0x2C
OLDSS	= 0x30

# 以下的定义都是相对于 task 结构体的偏移
state	= 0
counter	= 4
priority = 8
signal	= 12
sigaction = 16		# 必须是 16 (sigaction结构体的长度)
blocked = (33*16)

ENOSYS = 38

.globl system_call,sys_fork,timer_interrupt,sys_execve
.globl hd_interrupt,floppy_interrupt,parallel_interrupt
.globl device_not_available, coprocessor_error

bad_sys_call:
	pushl $-ENOSYS
	jmp ret_from_sys_call
.p2align 2
reschedule:
	# 把返回地址压栈，也就是调度完成后，仍旧会跳回到ret_from_sys_call处执行
	pushl $ret_from_sys_call
	jmp schedule

.p2align 2	# 4字节对齐
/**
 * 系统调用本质上也是一个陷阱门
 *	进入本函数前的堆栈情况是（毫无疑问，系统调用是从用户态进入内核态）
 *
 *	48(%esp) - %oldss   # 用户态进程tss中的ss3
 *	44(%esp) - %oldesp  # 用户态进程tss中的esp3
 *	40(%esp) - %eflags
 *	36(%esp) - %cs
 *	32(%esp) - %eip
 *	28(%esp) - %ds
 *	24(%esp) - %es
 *	20(%esp) - %fs
 *	16(%esp) - %eax     # 系统调用，栈此处的值原eax的值
 *	12(%esp) - %edx
 *	 8(%esp) - %ecx
 *	 4(%esp) - %ebx
 *	 0(%esp) - %eax
 *
 */
system_call:
	push %ds
	push %es
	push %fs

	/* eax 存的是系统调用号 */
	pushl %eax		# 保存 orig_eax

	/* 下面三个寄存器承载的是系统调用函数的入参 */
	pushl %edx		
	pushl %ecx		# %ebx,%ecx,%edx 作为系统调用的入参
	pushl %ebx
	/* 设置 ds es 段寄存器为内核代码段选择符 */
	/** 
	 * 这里面其实隐含了一个事情：就是数据段寄存器也是使用内核代码段选择符
	 * 这意味着，对于内存数据的操作，如果使用默认段寄存器（也就是ds）
	 * 只能读，不能写。这是否也是一种保护的方式呢？
	 * 那么要写内存怎么办？
	 * 后面把内核数据段选择符赋值给 fs 段寄存器，猜测一种方式就是，通过指定
	 * 段寄存器前缀为 %fs 来进行内存的写操作
	 **/
	movl $0x10,%edx		# 设置 ds,es 指向内核空间
	mov %dx,%ds
	mov %dx,%es
	/* 设置 fs 为LDT中的数据段选择符：因为后面在 do_signal 中会用到这个段选择符，去修改用户任务的堆栈  */
	movl $0x17,%edx
	mov %dx,%fs			# 系统调用会把 %fs 设置为 0x17，在内核中用来从用户空间拷贝数据用

	/*
	 * NR_syscalls 是系统调用表中的调用函数数量
	 * 这里是对比，系统调用号和总数量
	 * 如果系统调用好超出(>=)总数量，则不合法，执行 bad_sys_call
	 * 否则执行 sys_call_table[4 * %eax]，也就是对应的系统调用
	 */
	cmpl NR_syscalls,%eax
	jae bad_sys_call
	call sys_call_table(,%eax,4)
	pushl %eax		# 调用结束后，返回值在 %eax 寄存器中
2: /* 判断是否要对任务进行重新调度 */
	movl current,%eax
	/*
	 * 得到当前任务数据结构的指针，state = 0
	 * 因此这里是比较 (task + 0) 内存位置的值，也就是  task->state
	 * 0 表示任务是处于可运行状态
	 */
	cmpl $0,state(%eax)		# state # 判断当前任务是否处于可运行状态
	jne reschedule			# 如果不等于0，说明进程处于不能运行状态，那么要触发重新的任务调度
	
	/*
	 * counter = 4， task->counter 表示任务运行的时间片
	 * 如果当前进程的运行时间片也已经耗完
	 * 那么，也触发以下 任务重新调度
	 */
	cmpl $0,counter(%eax)		# counter
	je reschedule
ret_from_sys_call:
	movl current,%eax		# 指向当前任务的指针
	/*
	  task 是任务数组，此时是比较 task 和 current，看目前是不是在 任务0中
	  如果是任务0，那么就直接退出了，否则还需要处理 信号 相关逻辑
	  任务0就是 idle 任务
	 */
	cmpl task,%eax			# task[0] cannot have signals
	je 3f	/* 是任务0，从系统调用返回 */

	/* CS = 0x24, [%esp + 0x24] 指向进行系统调用的任务的 %cs 寄存器的值 */
	/* 实际这就是判断：是否为用户态的代码段: DPL=3，TI=1(LDT)，choose = 0x08 */
	cmpw $0x0f,CS(%esp)
	jne 3f /* 不是用户态代码段，退出！！！ */
	/*
	 * 这点也很重要，因为要把信号处理函数（用户态代码）插入进来
	 * 如果任务还在内核态，就去插入信号执行函数代码，按照接下来的逻辑
	 * 会导致用户态的信号处理函数执行完之后返回到原内核eip的地方，
	 * 并没有用户态到内核态的切换逻辑，会导致程序执行出错
	 */

	/* OLDSS = 0x30 [%esp + 0x30] 指向进行系统调用的任务的 %ss 寄存器的值 */
	/* 用于判断是否为用户态数据段，通过段选择符是否为0x17来判断 */
	cmpw $0x17,OLDSS(%esp)
	jne 3f

	/**
	 * signal	= 12, 也就是获取信号处理
	 * %ebx = task->signal
	 * %ecx = task->blocked，对 blocked 信号取非，就是需要处理的那些信号值
	 * %ebx & (~%ecx) ===> task->signal & (~task->blocked)
	 * 也就是取出注册要处理的信号，和未被屏蔽的信号的交集，表示最终要处理的信号集
	 **/
	movl signal(%eax),%ebx
	movl blocked(%eax),%ecx
	notl %ecx
	andl %ebx,%ecx

	/*
		BSFL 是 x86 架构中的一条汇编指令，全称为 Bit Scan Forward (Long)
		属于位扫描指令家族。它在 AT&T 语法中表示对 32 位（长字）操作数进行位扫描操作。
		其功能是从最低位（LSB）向高位扫描，寻找第一个被设置为 1 的位的位置
		输入：一个 32 位操作数（寄存器或内存地址）。
		输出：零标志位（ZF）：若源操作数为 0，则 ZF = 1；否则 ZF = 0

		因此是找出待处理信号集合中，bit位最低的那个位置，并存入 %ecx
	 */
	bsfl %ecx,%ecx
	je 3f	/* 如果没有信号需要处理，则退出 */

	/*
		BTRL 是 x86 架构中的一条汇编指令，全称为 Bit Test and Reset (Long)，
		属于位测试并修改操作指令家族。
		它在 AT&T 语法中表示对 32 位（长字）操作数进行位测试并复位（清零）操作。
		输入：
			目标操作数：一个 32 位的寄存器或内存地址（待操作的位数据）。
			源操作数：一个立即数或寄存器（指定要测试和复位的位索引，从 0 开始计数）。
		操作：
			测试目标操作数中由源操作数指定的位。
			将该位的值复制到 进位标志（CF） 中。
			将该位复位（清零为 0）
		标志位影响：
			CF（进位标志）：被测试位的原始值（1 或 0）。
			其他标志（OF、SF、ZF、PF、AF）可能变为未定义

		是原子操作！！！
	 */
	/*
	 * 把要处理的信号对应的bit位，查看是否在待处理信号集合中（肯定在，因为前面的逻辑保证了）
	 * 并把这个信号对应的bit位清零，存入 task->signal 中（这样就改变了 task->signal ）
	 * 这样执行完之后，所有信号都已经清零了，意味着如果需要再次执行信号处理函数
	 * 需要在执行完之后重新注册信号处理函数？？？
	 * --------
	 * 上面的理解有偏差：用户只是注册了对应信号的处理函数，跟实际哪些信号产生并没有关系
	 * 此处清零了所有信号，并不是清除了信号对应的执行函数，当信号再次发生时，仍旧会执行用户
	 *		注册的信号处理函数
	 */
	btrl %ecx,%ebx
	movl %ebx,signal(%eax)
	incl %ecx	/* 信号从1开始定义，因此要+1 */
	pushl %ecx /* 信号值作为参数，传给信号处理函数 do_signal */
	call do_signal
	popl %ecx
	/* %eax 存储的是do_signal的返回值，如果非0，则继续处理信号 */
	testl %eax, %eax
	jne 2b		# 判断是否需要切换任务，还是处理更多的信号

	/* 参数退栈，退出调用，从定时中断返回 */
3:	popl %eax
	popl %ebx
	popl %ecx
	popl %edx
	addl $4, %esp	# 跳过 orig_eax
	pop %fs
	pop %es
	pop %ds
	iret /* 如果有堆栈切换，会回到任务用户态执行 */

.p2align 2	# 4字节对齐
/**
 * 和 timer_interrupt 的栈分布一致
 */
coprocessor_error:
	push %ds
	push %es
	push %fs
	pushl $-1		# orig_eax 处填充 -1
	pushl %edx
	pushl %ecx
	pushl %ebx
	pushl %eax
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
	movl $0x17,%eax
	mov %ax,%fs
	pushl $ret_from_sys_call
	jmp math_error

.p2align 2	# 4字节对齐
device_not_available:
	push %ds
	push %es
	push %fs
	pushl $-1				# orig_eax 处填充 -1
	pushl %edx
	pushl %ecx
	pushl %ebx
	pushl %eax
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
	movl $0x17,%eax
	mov %ax,%fs
	pushl $ret_from_sys_call
	clts					# clear TS so that we can use math
	movl %cr0,%eax
	testl $0x4,%eax			# EM (math emulation bit)
	je math_state_restore
	pushl %ebp
	pushl %esi
	pushl %edi
	pushl $0		# temporary storage for ORIG_EIP
	call math_emulate
	addl $4,%esp
	popl %edi
	popl %esi
	popl %ebp
	ret


.p2align 2	# 4字节对齐
/*
 * 进入本函数时的栈布局情况是（当前已经处于内核态堆栈，且是从用户态切换而来）
 *	48(%esp) - %oldss   # 用户态进程tss中的ss3
 *	44(%esp) - %oldesp  # 用户态进程tss中的esp3
 *	40(%esp) - %eflags
 *	36(%esp) - %cs
 *	32(%esp) - %eip
 *	28(%esp) - %ds
 *	24(%esp) - %es
 *	20(%esp) - %fs
 *	16(%esp) - (-1)     # 非系统调用，栈此处的值是-1
 *	12(%esp) - %edx
 *	 8(%esp) - %ecx
 *	 4(%esp) - %ebx
 *	 0(%esp) - %eax
 */
timer_interrupt:	# 定时中断对应中断号 0x20
	push %ds		# 保存 ds,es，并且把内核态数据段存储到这两个寄存器中
	push %es		# %fs会被_system_call使用（仍然指向用户的数据段），主要是需要从用户空间复制数据时，用到%fs
	push %fs
	pushl $-1		# 这个位置填-1
	pushl %edx		# 我们保存%eax,%ecx,%edx，因为gcc不会保存他们
	pushl %ecx
	pushl %ebx		# %ebx在ret_sys_call中使用时已保存
	pushl %eax
	movl $0x10,%eax	# 内核态代码段选择符，加载到 ds 和 es 段寄存器中
	mov %ax,%ds
	mov %ax,%es
	movl $0x17,%eax	# 用户任务的LDT表中数据段选择符，加载到 fs 段寄存器中，do_signal中会用到
	mov %ax,%fs
	incl jiffies	# 节拍数值 +1
	movb $0x20,%al
	outb %al,$0x20	# 发送 EOI ，也就是通知8259A本次中断已经处理完成（如果IF标志置位，则本次中断可以被其他中断中断）
	/*
		X86 架构： 
			push 操作是先调整 esp 再写入值
			pop 操作是先弹出值，再调整 esp
		-------------------------------------------------------------------------------
		上面的注释有问题，CS = 0x24，而 CS(%esp) 表示的是取 %esp + CS 地址的值；CS=0x24=36
		%esp + 36 指向的正是中断时被压入栈的 原 %cs 段寄存器
		因此此处取的是 被中断任务的 %cs 段寄存器值
	 */
	movl CS(%esp),%eax	# 此时 esp 指向的是 被中断任务的 %cs 压栈的位置，此处将 %cs 取出来
	andl $3,%eax		# %eax is CPL (0 or 3, 0=supervisor) # 低 2 位是 CPL(也就代码段特权级，0是内核态，3是用户态)
	pushl %eax			# %eax(被中断任务时所处的特权级) 作为入参，压栈，传递给 do_timer
	call do_timer		# 'do_timer(long CPL)' 这个函数承担了定时中断的所有处理
	addl $4,%esp		# 弹出前面压栈的 %eax
	jmp ret_from_sys_call

.p2align 2	# 4字节对齐
sys_execve:
	lea EIP(%esp),%eax
	pushl %eax
	call do_execve
	addl $4,%esp
	ret
	